Skip to content

surface_input: scroll, off-surface ups, click count, and hover edges - #6

Merged
Vanuan merged 5 commits into
mainfrom
fix/reported-issues
Nov 3, 2026
Merged

Vanuan merged 5 commits into
mainfrom
fix/reported-issues

Conversation

@Vanuan

@Vanuan Vanuan commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Fix the four open input-gateway issues.

Validation: cargo test -p gpui_interop --features input-test --test surface_input - 16 passed.

SurfaceEvent had no Scroll variant, so a producer binding a wheel had to go around the gateway and the enum docs linked to an item that did not exist. Register on_scroll_wheel alongside the pointer handlers, carry the pointer position so a zoom can stay anchored, and map GPUI ScrollDelta to SurfaceScroll: a pixel delta is converted to the physical pixels the rest of the gateway uses, while a line delta passes through in lines.
on_mouse_up_out was gated on the engine drag, but the engine founds a drag only for a left press past the 2px threshold. A right or middle press, or a left press released after a sub-threshold move, therefore never produced a PointerUp when released outside the surface, leaving the producer stuck in its pressed state. Record which surface owns the current press on the App, clear it on the up, and report an off-surface up when either the drag or the press is ours.
PointerDown dropped MouseDownEvent::click_count, so a producer could not tell a double-click from two clicks without rebuilding the platform time/distance heuristic. Copy it to both PointerDown and PointerUp, matching the engine events they adapt.
The gateway reported moves, downs and ups, so a producer that highlights a gizmo or a navigation-cube face had no way to clear that state once the pointer left; a stationary leave can produce no further move at all. Subscribe on_hover and forward both edges as PointerEnter/PointerLeave, with modifiers but no position. The transition mirrors the engine hover state, including its suppression during a press or an active drag.
@Vanuan

Vanuan commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Reviewed against main and the CI run. Context: I maintain a downstream consumer (FerroCAD) that vendors this crate, so I compared this against the working copy we carry.

The implementation is the right shape and matches what we use — press ownership, scroll conversion and the click count are all in the places I'd expect. One real breakage, then docs, semantics and test nits.

Blocking: the example no longer compiles

examples/interop_cube_drag.rs:277-299 matches on SurfaceEvent with no wildcard:

match input.event {
    SurfaceEvent::PointerDown { position, .. } => { … }
    SurfaceEvent::PointerMove { position, .. } if grab.get() => { … }
    SurfaceEvent::PointerMove { .. } => {}
    SurfaceEvent::PointerUp { .. } => { … }
}

Adding PointerEnter, PointerLeave and Scroll makes that match non-exhaustive — E0004, the example does not build.

The CI is green because no job enables the example feature: Linux runs cargo test -p gpui_interop, Windows runs --features gpui, and package runs gpui,wgpu,guest. Examples also carry required-features = ["example"], so cargo test skips them. (The CI header's note about not building the Linux example is about interop_linux, whose platform bundle needs system libraries — but it hides this one too.)

Suggested fix, which also fixes the now-false comments:

  • Delete the example's own .on_scroll_wheel(...) at :254 and the two "the gateway's vocabulary carries no scroll yet" comments at :20-22 and :251-253; handle SurfaceEvent::Scroll { delta, .. } in the same match, reusing the existing notches logic (SurfaceScroll::Lines / SurfaceScroll::Pixels / PIXELS_PER_LINE).
  • Add an arm for the hover edges (or a wildcard) — the cube has no hover state.

And add cargo check --examples --features example to CI. The package job already uses ./.github/actions/linux-deps, so the system graphics libraries are available there; that check would have caught this.

Documentation

  1. The sign convention was dropped. The version we carry had: "The sign is the engine's — a positive y is 'up', which is already the opposite of the browser's deltaY." The PR replaces it with "in the units the platform reported it", and the scroll test asserts y.abs(), so nothing pins it. bite-gp-web still negates deltaY (ScrollDelta::Pixels(px(-event.delta_y()))), so restore the sentence on SurfaceScroll — it is exactly what surface_input: SurfaceEvent has no Scroll, though the enum docs link to it #2 asked for, and every zoom depends on it.

  2. SurfaceScroll::Pixels says "in physical pixels from the surface's top-left corner" — a delta has no origin; that clause belongs to positions. "in physical pixels" is enough.

  3. PointerLeave's press claim is inaccurate. It says "a press therefore reports a leave even if the pointer has not moved". In bite-gp-authoring, hover is recomputed only on a mouse move, a deferred layout check, or MouseExited; a mouse-down alone does not invoke the hover listener. What actually happens is that has_mouse_down makes is_hovered false, so the first move during a press reports a leave even while the pointer is inside. Reword to that. Also keep the "including by leaving the window" note that was removed — that path is real (MouseExited clears hover) and is the case producers care most about.

API / semantics

  1. on_hover is a single-listener slot, and the gateway now owns it. Interactivity::on_hover does debug_assert!(self.hover_listener.is_none()) and then overwrites. A producer that wraps with surface_input and then calls .on_hover(...) panics in debug and silently loses enter/leave in release. The module docs already say the gateway takes on_drag; say it takes on_hover too, and note that .hover(...) styles are separate and still work. (on_scroll_wheel, on_any_mouse_down and capture_any_mouse_up are listener vectors and do not conflict.)

  2. PressedSurface(Option<ElementId>) is one slot, not one per button. Any up over the surface clears it, and all three on_mouse_up_out handlers clear it regardless of button. In a chord — middle-drag orbit while clicking with the left, which CAD viewports do — releasing left clears ownership while middle is still held, so the middle's eventual off-surface up is lost again. Storing the button with the id (and comparing it in the up handlers) closes that. If you prefer to keep the id only, documenting the limitation is fine too.

  3. Lower priority: the global is app-wide, so two windows share it. The engine's drag is app-wide too, so this matches precedent; a window handle in the tuple would make it exact.

Tests

  1. ScrollDelta::Lines has no test. The scroll test only exercises Pixels; add a lines case asserting pass-through with no scale multiplication.

  2. PointerUp.click_count has no test. The double-click test asserts only PointerDown; add a MouseUpEvent { click_count: 2 } assertion.

  3. Use exact-count assertions in the two off-surface-up tests (.any() today). Asserting exactly one PointerUp catches double delivery across capture_any_mouse_up and on_mouse_up_out, and a follow-up up over the surface after an off-surface up would catch stale PressedSurface state.

  4. The scroll test's * scale may be a no-op if the test harness scale is 1.0 — if it can be set, set it to 2 for that test.

What looks good

Downstream note

Our vendored copy already has the new variants, so its interop_cube_drag.rs has the same non-exhaustive match. Nothing in our checks builds examples, so our tree is green, but it is broken the same way and we will fix it when we adopt this release.

The example now handles SurfaceEvent::Scroll through the gateway and has arms for the hover edges, so its match is exhaustive again; the package job checks examples with the example feature. Restore the scroll sign convention and fix the delta and PointerLeave wording; document that the gateway claims the on_hover slot. Press ownership now records one entry per window, surface and button, so a left release no longer forgets a held middle press and two windows do not share it. Add tests for a line scroll, PointerUp click_count, exact off-surface up counts, and a chord.
@Vanuan

Vanuan commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — all of it is addressed in 2ce1bbe.

Blocking: the example — interop_cube_drag now handles SurfaceEvent::Scroll itself (the separate on_scroll_wheel and its two comments are gone) and has empty arms for PointerEnter / PointerLeave, so the match is exhaustive. The package job now checks --examples --features example after cloning the core and installing linux-deps; that is the leg that would have caught it.

Docs — restored the sign sentence on SurfaceScroll (positive y is up, the opposite of the browser deltaY); dropped the top-left corner clause from the Pixels variant; reworded PointerLeave to say the first move during a press reports a leave, and kept the leaving-the-window path.

API / semantics — the module docs now say the gateway claims on_hover as well as on_drag, and that .hover(..) styles are separate. Press ownership is now a vector of (WindowId, ElementId, MouseButton): one entry per window, surface and button, so a left release cannot forget a held middle press and two windows do not share a press.

Tests — added the Lines pass-through case, the PointerUp.click_count assertion, exact-count assertions in both off-surface-up tests, and a chord test (left + middle released outside, each exactly once). The scroll test now asserts the harness scale is 2, so the scaling is not a no-op.

CI is green on the new head.

@Vanuan
Vanuan merged commit 2f73162 into main Nov 3, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant